### Load standardpackages
library(tidyverse) # Collection of all the good stuff like dplyr, ggplot2 ect.
library(magrittr) # For extra-piping operators (eg. %<>%)
library(tidytext)

This session

This session, we will

  1. Review NLP workflows and data structures in R
  2. Explore different type of DTM matrix type vector representations of text.
  3. Add different types of dimensionality reduction techniques to the repertoir.
  4. HAve a peak into word-embeddings
  5. Add some goddies on top

Refresher:

Bag of words model

  • In order for a computer to understand text we need to somehow find a useful representation.
  • If you need to compare different texts e.g. articles, you will probably go for keywords. These keywords may come from a keyword-list with for example 200 different keywords
  • In that case you could represent each document with a (sparse) vector with 1 for “keyword present” and 0 for “keyword absent”
  • We can also get a bit more sophoistocated and count the number of times a word from our dictionary occurs.
  • For a corpus of documents that would give us a document-term matrix.

example

Let’s try creating a bag of words model from our initial example.

text <- tibble(id = c(1:6),
               text = c('A text about cats.',
                        'A text about dogs.',
                        'And another text about a dog.',
                        'Why always writing about cats and dogs, always dogs?',
                        'There are too little text about cats but to many about dogs',
                        'Cats, cats, cats! I love cats soo much. Cats are way better than dogs'))
text_tidy <- text %>% 
  unnest_tokens(word, text, token = 'words') %>% 
  count(id, word)

The document-term matrix (DTM)

  • The simplest form of vector representation of text is a ddocument-term matrix
  • How to we get a document-term matrix now?
  • We could do it by hand, with well-known dplyr syntax (Note: only works when you have one row per unique document-word pair)
text_tidy %>%
  pivot_wider(names_from = word, values_from = n, values_fill = 0)
  • We could also use cast_dtm() to create a DTM in the format of the tm package.
text_dtm <- text_tidy %>%
  cast_dtm(id, word, n)
text_dtm 
<<DocumentTermMatrix (documents: 6, terms: 25)>>
Non-/sparse entries: 42/108
Sparsity           : 72%
Maximal term length: 7
Weighting          : term frequency (tf)
  • We can simply convert ig to a tibble. Since there exists no direct transfer function, we have to first transform it to a matrix.
  • Notice how we recover the rownames
text_dtm %>% as.matrix() %>% as_tibble(rownames = 'id') 
  • Sidenote: We can also tidy the DTM again to a tidy token-dataframe.
text_dtm %>% tidy()
  • We also can directly use a similar function to cast a sparse matrix (which we for sure then also could transform to a tibble again)
text_tidy %>% cast_sparse(row = id, column = word, value = n)
6 x 25 sparse Matrix of class "dgCMatrix"
                                                   
1 1 1 1 1 . . . . . . . . . . . . . . . . . . . . .
2 1 1 . 1 1 . . . . . . . . . . . . . . . . . . . .
3 1 1 . 1 . 1 1 1 . . . . . . . . . . . . . . . . .
4 . 1 1 . 2 1 . . 2 1 1 . . . . . . . . . . . . . .
5 . 2 1 1 1 . . . . . . 1 1 1 1 1 1 1 . . . . . . .
6 . . 5 . 1 . . . . . . 1 . . . . . . 1 1 1 1 1 1 1
  • Finally, we could just apply a text recipe here
library(recipes)
library(textrecipes)
text %>%
  recipe(~.) %>% 
  step_tokenize(text, token = 'words') %>% # tokenize
  step_tf(text) %>% # TFIDF weighting
  prep() %>% juice()

TF-IDF - Term Frequency - Inverse Document Frequency

  • A token is important for a document if appears very often
  • A token becomes less important for comparison across a corpus if it appears all over the place in the corpus
  • Cat in a corpus of websites talking about cats is not that important

\[w_{i,j} = tf_{i,j}*log(\frac{N}{df_i})\]

  • \(w_{i,j}\) = the TF-IDF score for a term i in a document j
  • \(tf_{i,j}\) = number of occurence of term i in document j
  • \(N\) = number of documents in the corpus
  • \(df_i\) = number of documents with term i
# TFIDF weights
text_tidy %<>%
  bind_tf_idf(term = word,
              document = id,
              n = n)
  • We obviously could also cast a tf_idf weighted dtm…
text_tidy %>%
  select(id, word, tf_idf) %>%
  pivot_wider(names_from = word, values_from = tf_idf, values_fill = 0)
  • btw: this is equivalent to just running a textrecipe like that:
text %>%
  recipe(~.) %>% 
  step_tokenize(text, token = 'words') %>% # tokenize
  step_tfidf(text) %>% # TFIDF weighting
  prep() %>% juice()
  • Sidenote, when we use a POS engine such as spacyr for tokenization, we can also add recipes for lematization, filter for POS etc.
text %>%
  recipe(~.) %>% 
  step_tokenize(text, engine = "spacyr") %>%
  step_pos_filter(text, keep_tags = "NOUN") %>%
  step_lemma(text) %>%
  step_tf(text) %>%
  prep() %>%
  juice()
  • A last reminder on the powerful pairwise_xx() functions from the widyr package
  • For instance, pairwise similarities/distances
library(widyr)
text_tidy %>% pairwise_dist(id, word, tf_idf, method = "manhattan") %>%
  mutate(similarity = 1 - (distance / max(distance)) ) %>%
  select(-distance) %>%
  arrange(desc(similarity))

Dimensionality reduction techniques

rm(list=ls())
  • Ok, lets get first some more interesting data. We will work with the CORDIS project descriptions of EU Horizon 2020 projects again.
text <- read_csv('https://github.com/SDS-AAU/SDS-master/raw/master/M2/data/cordis-h2020reports.gz')
colnames(text) <- colnames(text) %>% str_to_lower()
text %<>%
  select(-x1) %>%
  rename(id = projectid) %>%
  relocate(id) %>%
  filter(language == 'en') %>%
  drop_na(id)
  • Lets create a tidy tokenlist
text_tidy <- text %>%
  rename(text = summary) %>%
  select(id, text) %>%
  unnest_tokens(word, text, token = "words")
  • some preprocessing
# preprocessing
text_tidy %<>%
  filter(str_length(word) > 2 ) %>% # Remove words with less than  3 characters
  filter(!(word %in% c('project', 'research'))) %>%
  anti_join(stop_words, by = 'word') 
  • We can also ad bigrams
text_tidy %<>%
  unnest_tokens(word, word, token = 'ngrams', n = 2, n_min = 1) %>%
  group_by(word) %>% filter(n() > 25) %>% ungroup() 
text_tidy %>%
  count(word, sort = TRUE)
  • Lets finish this up and also add TF-IDF weights
text_tidy %<>%
  count(id, word) %>%
  bind_tf_idf(term = word,
              document = id,
              n = n) %>%
  select(-tf, -idf)
  • Is there a big difference?
text_tidy %>%
  count(word, wt = tf_idf, sort = TRUE)
  • And finally, lets get a DTM dataframe
text_dtm <- text_tidy %>%
  select(id, word, n) %>%
  pivot_wider(names_from = word, values_from = n, values_fill = 0)
  • And, just in case, a TFIDF weighted version
  • We could also prepare a recipe which doe pretty much the same…
recipe_base <- text %>%
  rename(text = summary) %>%
  select(id, text) %>%
  # BAse recipe starts
  recipe(~.) %>% 
  update_role(id, new_role = "id") %>% # Update role of ID
  step_tokenize(text, token = 'words') %>% # tokenize
  step_stopwords(text, keep = FALSE) %>% # remove stopwords
  step_untokenize(text) %>% # Here we now have to first untokenize
  step_tokenize(text, token = "ngrams", options = list(n = 1, n_min = 1)) %>% # and tokenize again
  step_tokenfilter(text, min_times = 25) 
  • Sidenote

  • Here, we can further preprocess to do whatever we would like, such as obtaining a dtm

recipe_base %>% 
  step_tf(text) %>% 
  prep() %>% 
  juice() %>% 
  head(100)
text_pca <- text_dtm %>% 
  column_to_rownames('id') %>% 
  prcomp(center = TRUE, scale. = TRUE, rank. = 10)
text_pca %>% glimpse()
List of 5
 $ sdev    : num [1:499] 3.54 3.05 2.86 2.8 2.66 ...
 $ rotation: num [1:599, 1:10] 0.01561 0.00169 0.07306 -0.03103 0.01753 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : chr [1:599] "aim" "allowing" "based" "blood" ...
  .. ..$ : chr [1:10] "PC1" "PC2" "PC3" "PC4" ...
 $ center  : Named num [1:599] 0.2265 0.0541 0.6733 0.0701 0.1543 ...
  ..- attr(*, "names")= chr [1:599] "aim" "allowing" "based" "blood" ...
 $ scale   : Named num [1:599] 0.537 0.235 1.049 0.445 0.856 ...
  ..- attr(*, "names")= chr [1:599] "aim" "allowing" "based" "blood" ...
 $ x       : num [1:499, 1:10] -3.24 -0.94 -1.62 -1.25 -1.4 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : chr [1:499] "115844" "633197" "633249" "633261" ...
  .. ..$ : chr [1:10] "PC1" "PC2" "PC3" "PC4" ...
 - attr(*, "class")= chr "prcomp"
text_pca[['x']] %>%
  head()
             PC1       PC2        PC3        PC4        PC5        PC6        PC7        PC8         PC9       PC10
115844 -3.240084 -1.224083  1.0929219 -0.2966866  0.7300978 -0.1272715  0.1145893 -2.3920794 -0.01109556  0.1170174
633197 -0.939826  4.805016  1.9730777  2.0355810 -0.9698387  0.5595339  1.9143958 -2.0685189 -1.93477227 -1.4869024
633249 -1.620804  4.488997  0.3095817  2.5046982 -1.6895767  1.9497913 -0.5535784 -0.6742253  0.01409018 -1.1999591
633261 -1.249307  4.793337  1.3972610  3.9865897 -0.6584607  0.7980526  1.1628729 -2.7636819 -3.20517847 -0.6888338
633382 -1.399995  5.265843  0.1248408  4.1248219 -0.9627588  1.3542756  1.4368296 -0.6137203 -1.93784759 -0.8948685
633571  1.445999  2.273599 -0.7621658  1.2438623 -2.1143075  0.4455806  0.8693148  0.8680666  2.56183743  2.5522762
text_pca %>% tidy()
  • Again, alternatively with a recipe…
recipe_pca <- recipe_base %>% # tokenize
  step_tfidf(text, prefix = '') %>% # TFIDF weighting
  step_pca(all_predictors(), num_comp = 10) %>% # PCA
  prep() 
recipe_pca %>% juice()
  • Some plotting
recipe_pca %>% juice() %>%
  ggplot(aes(x = PC01, y = PC02)) +
  geom_point() 

  • we can also use the tidy results of the recipe to do some more analytics
recipe_pca %>%
  tidy(7) %>%
  filter(component %in% paste0("PC", 1:4)) %>%
  group_by(component) %>%
    arrange(desc(value)) %>%
    slice(c(1:2, (n()-2):n())) %>%
  ungroup() %>%
  mutate(component = fct_inorder(component)) %>%
  ggplot(aes(value, terms, fill = terms)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~component, nrow = 1) +
  labs(y = NULL)

  • Note: Also check further for further dimensionlity reduction steps:
    • tep_kpca():
    • step_ica()
    • step_isomap()
    • step_nnmf()

Topic Models: Latent-Dirichlet-Allocation (LDA)

  • While we already did it somewhat ‘on-the-fly’, here a more formal introduction to LDA
  • In contrast to dimnesionality reduction techiques mostly aiming at preprocessing data or easing visualization, LDA more aims at EDA and interpretation
  • It is a generative approach to identify topics (clusters) within the word-usage in documents.
    • Topics are represented as a probability distribution over the words in the vocabulary. Hhigh probability words can be used to charactrize the topic.
    • Documents are represented as a mixture of topics.

alt text

library(topicmodels)
text_dtm <- text_tidy %>%
  cast_dtm(document = id, term = word, value = n)
text_lda <- text_dtm %>% 
  LDA(k = 6, method = "Gibbs",
      control = list(seed = 1337))
  • \(\beta\) is an output of the LDA model, indicating the propability that a word occurs in a certain topic.
  • Therefore, loking at the top probability words of a topic often gives us a good intuition regarding its properties.
# LDA output is defined for tidy(), so we can easily extract it
lda_beta <- text_lda %>% 
  tidy(matrix = "beta") 
lda_beta %>%
  # slice
  group_by(topic) %>%
  arrange(topic, desc(beta)) %>%
  slice(1:10) %>%
  ungroup() %>%
  # visualize
  mutate(term = reorder_within(term, beta, topic)) %>%
  group_by(topic, term) %>%    
  arrange(desc(beta)) %>%  
  ungroup() %>%
  ggplot(aes(term, beta, fill = as.factor(topic))) +
  geom_col(show.legend = FALSE) +
  coord_flip() +
  scale_x_reordered() +
  labs(title = "Top 10 terms in each LDA topic",
       x = NULL, y = expression(beta)) +
  facet_wrap(~ topic, ncol = 3, scales = "free")

  • Documents are represented as a mix of topics. This association of a document to a topic is captured by \(\gamma\)
lda_gamma <- text_lda %>% 
  tidy(matrix = "gamma")
lda_gamma %>%
  group_by(topic) %>%
    arrange(desc(gamma)) %>% 
    slice(1:10) %>%
  ungroup() %>%
  left_join(text %>% select(id, projectacronym) %>% mutate(id = id %>% as.character()), by = c('document' = 'id'))
  • Note that an LDA can also be performed via a recipe:
recipe_lda <- recipe_base %>% # tokenize
  step_lda(text, num_topics = 6) %>% # LDA
  prep() 
recipe_lda %>% juice() %>% 
  head(100)
  • As a bonus, a great way to interactively visualize LDA’s.
  • It’s a bit cumbersome in R, though…
library(LDAvis)
# A bit of a lenghty function....
topicmodels_json_ldavis <- function(fitted, doc_dtm, method = "PCA"){
  require(topicmodels); require(dplyr); require(LDAvis)
  
  # Find required quantities
  phi <- posterior(text_lda)$terms %>% as.matrix() # Topic-term distribution
  theta <- posterior(fitted)$topics %>% as.matrix() # Document-topic matrix
  
  text_tidy <- doc_dtm %>% tidy()
  vocab <- colnames(phi)
  doc_length <- tibble(document = rownames(theta)) %>% left_join(text_tidy %>% count(document, wt = count), by = 'document')
  tf <- tibble(term = vocab) %>% left_join(text_tidy %>% count(term, wt = count), by = "term") 
  
  if(method == "PCA"){mds <- jsPCA}
  if(method == "TSNE"){library(tsne); mds <- function(x){tsne(svd(x)$u)} }
  
  # Convert to json
  json_lda <- LDAvis::createJSON(phi = phi, theta = theta, vocab = vocab, doc.length = doc_length %>% pull(n), term.frequency = tf %>% pull(n),
                                 reorder.topics = FALSE, mds.method = mds,plot.opts = list(xlab = "Dim.1", ylab = "Dim.2")) 
  return(json_lda)
}
library(LDAvis)
json_lda <- topicmodels_json_ldavis(fitted = text_lda, 
                                    doc_dtm = text_dtm, 
                                    method = "TSNE")

json_lda %>% serVis() # For direct output
# json_lda %>% serVis(out.dir = 'LDAviz') # For saving the html

Didnt really figure out how to embedd the resulting plot, but the outcome can be seen here

Embeddings (Bonus)

  • One last thing we did not venture in yet, are embeddings

  • I will not go into details here, just see it as a peak of what’s to come in further sessions.

  • The idee of word embedding is (in a nutshell) that

  • There are packages on how to train own embeddings such as text2vec, but we will for now not bother with that.

  • The only thing we will do for now is to load pretrained embeddings (GloVe, cf. Pennington et al, 2014)

library(textdata)

glove6b <- embedding_glove6b(dimensions = 100)
glove6b
  • La voila, a large pretrained embedding model for around 400k of the most common words.
  • We for now loaded the smallest of these embedding models, there exist way bigger ones.
  • Lets join it with our tidy tokenlist
word_embeddings <- text_tidy %>%
  inner_join(glove6b, by = c('word' = 'token'))
word_embeddings %>% head()
  • We could now create average document embeddings by taking the mean over all dimensions
  • We could also (even better) weight that by then word’s tfidf score.
doc_embeddings <- word_embeddings %>%
  group_by(id) %>%
  summarise(across(starts_with("d"), ~mean(.x / tf_idf, na.rm = TRUE)))
  • These embddings could now be used for instance for some clustering or SML exercise
  • I guess you can already see how to use these embeddings in an SML model.
library(uwot) # for UMAP
embeddings_umap <- doc_embeddings  %>% 
  column_to_rownames("id") %>%
  umap(n_neighbors = 15, 
       metric = "cosine", 
       min_dist = 0.01, 
       scale = TRUE,
       verbose = TRUE, 
       n_threads = 8) 
0%   10   20   30   40   50   60   70   80   90   100%
[----|----|----|----|----|----|----|----|----|----|
**************************************************|
embeddings_umap %<>% as.data.frame()
embeddings_umap  %>% 
  ggplot(aes(x = V1, y = V2)) + 
  geom_point(shape = 21, alpha = 0.5) 

  • Ok, we see a rather clear seperation of documents.
  • Just for fun, lets add a density based clustering (very good for spatial clustering) on top (even though we already see the results)
library(dbscan)
  • Do the hirarchical density based clustering
embeddings_hdbscan <- embeddings_umap %>% as.matrix() %>% hdbscan(minPts = 15)
  • Plot it
embeddings_umap %>% 
  bind_cols(cluster = embeddings_hdbscan$cluster %>% as.factor(), 
            prob = embeddings_hdbscan$membership_prob) %>%
  ggplot(aes(x = V1, y = V2, col = cluster)) + 
  geom_point(aes(alpha = prob), shape = 21) 

  • Note: We can also assigne the embeddings via a recipe
  • Unfortunately, we can not do a TFIDF weighting here ‘out-of-the-box’, but have to work with average embeddings instead.
recipe_embedding <- recipe_base %>% # tokenize
  step_word_embeddings(text, embeddings = glove6b, aggregation = 'mean')
recipe_embedding %>% prep() %>% juice() %>% 
  head(100)
library(embed)
recipe_umap <- recipe_embedding %>%
  step_umap(starts_with('w_embed'), n_neighbors = 15) 
recipe_umap %>% prep() %>% juice() %>% 
  head(100)

—>

  • So, that’s all I have for now

Summary

  • There are many ways to convert text data into a vector representation.
  • These range from simple and weighted bags-of-words, to topic models, over different types of dimensionality reduction to finally word and document embeddings.
  • All of them are useful, depending on the purpose.

Endnotes

Packages & Ecosystem

  • textrecipes: Text preprocessing recipes
  • embed: Extra embedding recipes
  • topicmodels: LDA topicmodelling in R
  • LDAvis: A bit clunky but awesome interactive LDA visualizations
  • text2vec: Package vor vector space modelling (aka embeddings & other vectorizations) of textdata
  • textdata: Useful datasets for text, such as GloVe embeddings, sentiment lexica etc.
  • uwot: UMAP for R

References

CHapters:

  • Julia Silge and David Robinson (2020). Text Mining with R: A Tidy Approach, O’Reilly. Online available here
  • Emil Hvidfeldt and Julia Silge (2020). Supervised Machine Learning for Text Analysis in R, online available here

Articles: * Blei, David M., Andrew Y. Ng, and Michael I. Jordan. “Latent dirichlet allocation.” Journal of machine Learning research 3, no. Jan (2003): 993-1022. * Jeffrey Pennington, Richard Socher, and Christopher D Manning. Glove: Global vectors for word representation. In Conference on Empirical Methods on Natural Language Processing (EMNLP), pages 1532–1543, 2014

Further sources

Session Info

sessionInfo()
LS0tCnRpdGxlOiAnKFNvbWV3aGF0KSBhZHZhbmNlZCBOTFA6IHRleHQgdmVjdG9yaXphdGlvbicKYXV0aG9yOiAiRGFuaWVsIFMuIEhhaW4gKGRzaEBidXNpbmVzcy5hYXUuZGspIgpkYXRlOiAiVXBkYXRlZCBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVCICVkLCAlWScpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiBmYWxzZQogICAgdGhlbWU6IGZsYXRseQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQojIyMgR2VuZXJpYyBwcmVhbWJsZQpybShsaXN0PWxzKCkpClN5cy5zZXRlbnYoTEFORyA9ICJlbiIpICMgRm9yIGVuZ2xpc2ggbGFuZ3VhZ2UKb3B0aW9ucyhzY2lwZW4gPSA1KSAjIFRvIGRlYWN0aXZhdGUgYW5ub3lpbmcgc2NpZW50aWZpYyBudW1iZXIgbm90YXRpb24KCiMjIyBLbml0ciBvcHRpb25zCmxpYnJhcnkoa25pdHIpICMgRm9yIGRpc3BsYXkgb2YgdGhlIG1hcmtkb3duCmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBtZXNzYWdlPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBjb21tZW50PUZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgZmlnLmFsaWduPSJjZW50ZXIiCiAgICAgICAgICAgICAgICAgICAgICkKYGBgCgpgYGB7cn0KIyMjIExvYWQgc3RhbmRhcmRwYWNrYWdlcwpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBDb2xsZWN0aW9uIG9mIGFsbCB0aGUgZ29vZCBzdHVmZiBsaWtlIGRwbHlyLCBnZ3Bsb3QyIGVjdC4KbGlicmFyeShtYWdyaXR0cikgIyBGb3IgZXh0cmEtcGlwaW5nIG9wZXJhdG9ycyAoZWcuICU8PiUpCmBgYAoKYGBge3J9CmxpYnJhcnkodGlkeXRleHQpCmBgYAoKIyBUaGlzIHNlc3Npb24KClRoaXMgc2Vzc2lvbiwgd2Ugd2lsbAoKMS4gUmV2aWV3IE5MUCB3b3JrZmxvd3MgYW5kIGRhdGEgc3RydWN0dXJlcyBpbiBSCjIuIEV4cGxvcmUgZGlmZmVyZW50IHR5cGUgb2YgRFRNIG1hdHJpeCB0eXBlIHZlY3RvciByZXByZXNlbnRhdGlvbnMgb2YgdGV4dC4KMy4gQWRkIGRpZmZlcmVudCB0eXBlcyBvZiBkaW1lbnNpb25hbGl0eSByZWR1Y3Rpb24gdGVjaG5pcXVlcyB0byB0aGUgcmVwZXJ0b2lyLgo2LiBIQXZlIGEgcGVhayBpbnRvIHdvcmQtZW1iZWRkaW5ncwo1LiBBZGQgc29tZSBnb2RkaWVzIG9uIHRvcAoKIyBSZWZyZXNoZXI6CgohW10oaHR0cHM6Ly9zZHMtYWF1LmdpdGh1Yi5pby9TRFMtbWFzdGVyLzAwX21lZGlhL25scF90aWR5d29ya2Zsb3cucG5nKQoKCiMgQmFnIG9mIHdvcmRzIG1vZGVsCgoqIEluIG9yZGVyIGZvciBhIGNvbXB1dGVyIHRvIHVuZGVyc3RhbmQgdGV4dCB3ZSBuZWVkIHRvIHNvbWVob3cgZmluZCBhIHVzZWZ1bCByZXByZXNlbnRhdGlvbi4KKiBJZiB5b3UgbmVlZCB0byBjb21wYXJlIGRpZmZlcmVudCB0ZXh0cyBlLmcuIGFydGljbGVzLCB5b3Ugd2lsbCBwcm9iYWJseSBnbyBmb3Iga2V5d29yZHMuIFRoZXNlIGtleXdvcmRzIG1heSBjb21lIGZyb20gYSBrZXl3b3JkLWxpc3Qgd2l0aCBmb3IgZXhhbXBsZSAyMDAgZGlmZmVyZW50IGtleXdvcmRzCiogSW4gdGhhdCBjYXNlIHlvdSBjb3VsZCByZXByZXNlbnQgZWFjaCBkb2N1bWVudCB3aXRoIGEgKHNwYXJzZSkgdmVjdG9yIHdpdGggMSBmb3IgImtleXdvcmQgcHJlc2VudCIgYW5kIDAgZm9yICJrZXl3b3JkIGFic2VudCIKKiBXZSBjYW4gYWxzbyBnZXQgYSBiaXQgbW9yZSBzb3Bob2lzdG9jYXRlZCBhbmQgY291bnQgdGhlIG51bWJlciBvZiB0aW1lcyBhIHdvcmQgZnJvbSBvdXIgZGljdGlvbmFyeSBvY2N1cnMuCiogRm9yIGEgY29ycHVzIG9mIGRvY3VtZW50cyB0aGF0IHdvdWxkIGdpdmUgdXMgYSBkb2N1bWVudC10ZXJtIG1hdHJpeC4KCiFbZXhhbXBsZV0oaHR0cHM6Ly9pLnN0YWNrLmltZ3VyLmNvbS9DMVVNcy5wbmcpCgpMZXQncyB0cnkgY3JlYXRpbmcgYSBiYWcgb2Ygd29yZHMgbW9kZWwgZnJvbSBvdXIgaW5pdGlhbCBleGFtcGxlLgoKYGBge3J9CnRleHQgPC0gdGliYmxlKGlkID0gYygxOjYpLAogICAgICAgICAgICAgICB0ZXh0ID0gYygnQSB0ZXh0IGFib3V0IGNhdHMuJywKICAgICAgICAgICAgICAgICAgICAgICAgJ0EgdGV4dCBhYm91dCBkb2dzLicsCiAgICAgICAgICAgICAgICAgICAgICAgICdBbmQgYW5vdGhlciB0ZXh0IGFib3V0IGEgZG9nLicsCiAgICAgICAgICAgICAgICAgICAgICAgICdXaHkgYWx3YXlzIHdyaXRpbmcgYWJvdXQgY2F0cyBhbmQgZG9ncywgYWx3YXlzIGRvZ3M/JywKICAgICAgICAgICAgICAgICAgICAgICAgJ1RoZXJlIGFyZSB0b28gbGl0dGxlIHRleHQgYWJvdXQgY2F0cyBidXQgdG8gbWFueSBhYm91dCBkb2dzJywKICAgICAgICAgICAgICAgICAgICAgICAgJ0NhdHMsIGNhdHMsIGNhdHMhIEkgbG92ZSBjYXRzIHNvbyBtdWNoLiBDYXRzIGFyZSB3YXkgYmV0dGVyIHRoYW4gZG9ncycpKQpgYGAKCmBgYHtyfQp0ZXh0X3RpZHkgPC0gdGV4dCAlPiUgCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0LCB0b2tlbiA9ICd3b3JkcycpICU+JSAKICBjb3VudChpZCwgd29yZCkKYGBgCgoKIyMgVGhlIGRvY3VtZW50LXRlcm0gbWF0cml4IChEVE0pCgoqIFRoZSBzaW1wbGVzdCBmb3JtIG9mIHZlY3RvciByZXByZXNlbnRhdGlvbiBvZiB0ZXh0IGlzIGEgZGRvY3VtZW50LXRlcm0gbWF0cml4CiogSG93IHRvIHdlIGdldCBhIGRvY3VtZW50LXRlcm0gbWF0cml4IG5vdz8KKiBXZSBjb3VsZCBkbyBpdCBieSBoYW5kLCB3aXRoIHdlbGwta25vd24gYGRwbHlyYCBzeW50YXggKE5vdGU6IG9ubHkgd29ya3Mgd2hlbiB5b3UgaGF2ZSBvbmUgcm93IHBlciB1bmlxdWUgZG9jdW1lbnQtd29yZCBwYWlyKQoKYGBge3J9CnRleHRfdGlkeSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gd29yZCwgdmFsdWVzX2Zyb20gPSBuLCB2YWx1ZXNfZmlsbCA9IDApCmBgYAoKKiBXZSBjb3VsZCBhbHNvIHVzZSBgY2FzdF9kdG0oKWAgdG8gY3JlYXRlIGEgRFRNIGluIHRoZSBmb3JtYXQgb2YgdGhlIGB0bWAgcGFja2FnZS4KCmBgYHtyfQp0ZXh0X2R0bSA8LSB0ZXh0X3RpZHkgJT4lCiAgY2FzdF9kdG0oaWQsIHdvcmQsIG4pCmBgYAoKYGBge3J9CnRleHRfZHRtIApgYGAKCiogV2UgY2FuIHNpbXBseSBjb252ZXJ0IGlnIHRvIGEgdGliYmxlLiBTaW5jZSB0aGVyZSBleGlzdHMgbm8gZGlyZWN0IHRyYW5zZmVyIGZ1bmN0aW9uLCB3ZSBoYXZlIHRvIGZpcnN0IHRyYW5zZm9ybSBpdCB0byBhIG1hdHJpeC4KKiBOb3RpY2UgaG93IHdlIHJlY292ZXIgdGhlIHJvd25hbWVzCgpgYGB7cn0KdGV4dF9kdG0gJT4lIGFzLm1hdHJpeCgpICU+JSBhc190aWJibGUocm93bmFtZXMgPSAnaWQnKSAKYGBgCgoqIFNpZGVub3RlOiBXZSBjYW4gYWxzbyB0aWR5IHRoZSBEVE0gYWdhaW4gdG8gYSB0aWR5IHRva2VuLWRhdGFmcmFtZS4KCmBgYHtyfQp0ZXh0X2R0bSAlPiUgdGlkeSgpCmBgYAoqIFdlIGFsc28gY2FuIGRpcmVjdGx5IHVzZSBhIHNpbWlsYXIgZnVuY3Rpb24gdG8gY2FzdCBhIHNwYXJzZSBtYXRyaXggKHdoaWNoIHdlIGZvciBzdXJlIHRoZW4gYWxzbyBjb3VsZCB0cmFuc2Zvcm0gdG8gYSB0aWJibGUgYWdhaW4pCgpgYGB7cn0KdGV4dF90aWR5ICU+JSBjYXN0X3NwYXJzZShyb3cgPSBpZCwgY29sdW1uID0gd29yZCwgdmFsdWUgPSBuKQpgYGAKCiogRmluYWxseSwgd2UgY291bGQganVzdCBhcHBseSBhIHRleHQgcmVjaXBlIGhlcmUKCmBgYHtyfQpsaWJyYXJ5KHJlY2lwZXMpCmxpYnJhcnkodGV4dHJlY2lwZXMpCmBgYAoKYGBge3J9CnRleHQgJT4lCiAgcmVjaXBlKH4uKSAlPiUgCiAgc3RlcF90b2tlbml6ZSh0ZXh0LCB0b2tlbiA9ICd3b3JkcycpICU+JSAjIHRva2VuaXplCiAgc3RlcF90Zih0ZXh0KSAlPiUgIyBURklERiB3ZWlnaHRpbmcKICBwcmVwKCkgJT4lIGp1aWNlKCkKYGBgCgoKIyMgVEYtSURGIC0gVGVybSBGcmVxdWVuY3kgLSBJbnZlcnNlIERvY3VtZW50IEZyZXF1ZW5jeQoKKiBBIHRva2VuIGlzIGltcG9ydGFudCBmb3IgYSBkb2N1bWVudCBpZiBhcHBlYXJzIHZlcnkgb2Z0ZW4KKiBBIHRva2VuIGJlY29tZXMgbGVzcyBpbXBvcnRhbnQgZm9yIGNvbXBhcmlzb24gYWNyb3NzIGEgY29ycHVzIGlmIGl0IGFwcGVhcnMgYWxsIG92ZXIgdGhlIHBsYWNlIGluIHRoZSBjb3JwdXMKKiAqQ2F0KiBpbiBhIGNvcnB1cyBvZiB3ZWJzaXRlcyB0YWxraW5nIGFib3V0IGNhdHMgaXMgbm90IHRoYXQgaW1wb3J0YW50CgokJHdfe2ksan0gPSB0Zl97aSxqfSpsb2coXGZyYWN7Tn17ZGZfaX0pJCQKCi0gJHdfe2ksan0kID0gdGhlIFRGLUlERiBzY29yZSBmb3IgYSB0ZXJtIGkgaW4gYSBkb2N1bWVudCBqCi0gJHRmX3tpLGp9JCA9IG51bWJlciBvZiBvY2N1cmVuY2Ugb2YgdGVybSBpIGluIGRvY3VtZW50IGoKLSAkTiQgPSBudW1iZXIgb2YgZG9jdW1lbnRzIGluIHRoZSBjb3JwdXMKLSAkZGZfaSQgPSBudW1iZXIgb2YgZG9jdW1lbnRzIHdpdGggdGVybSBpCgpgYGB7cn0KIyBURklERiB3ZWlnaHRzCnRleHRfdGlkeSAlPD4lCiAgYmluZF90Zl9pZGYodGVybSA9IHdvcmQsCiAgICAgICAgICAgICAgZG9jdW1lbnQgPSBpZCwKICAgICAgICAgICAgICBuID0gbikKYGBgCgoqIFdlIG9idmlvdXNseSBjb3VsZCBhbHNvIGNhc3QgYSB0Zl9pZGYgd2VpZ2h0ZWQgZHRtLi4uCgpgYGB7cn0KdGV4dF90aWR5ICU+JQogIHNlbGVjdChpZCwgd29yZCwgdGZfaWRmKSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gd29yZCwgdmFsdWVzX2Zyb20gPSB0Zl9pZGYsIHZhbHVlc19maWxsID0gMCkKYGBgCgoqIGJ0dzogdGhpcyBpcyBlcXVpdmFsZW50IHRvIGp1c3QgcnVubmluZyBhIHRleHRyZWNpcGUgbGlrZSB0aGF0OgoKYGBge3J9CnRleHQgJT4lCiAgcmVjaXBlKH4uKSAlPiUgCiAgc3RlcF90b2tlbml6ZSh0ZXh0LCB0b2tlbiA9ICd3b3JkcycpICU+JSAjIHRva2VuaXplCiAgc3RlcF90ZmlkZih0ZXh0KSAlPiUgIyBURklERiB3ZWlnaHRpbmcKICBwcmVwKCkgJT4lIGp1aWNlKCkKYGBgCgoqIFNpZGVub3RlLCB3aGVuIHdlIHVzZSBhIFBPUyBlbmdpbmUgc3VjaCBhcyBgc3BhY3lyYCBmb3IgdG9rZW5pemF0aW9uLCB3ZSBjYW4gYWxzbyBhZGQgcmVjaXBlcyBmb3IgbGVtYXRpemF0aW9uLCBmaWx0ZXIgZm9yIFBPUyBldGMuCgpgYGB7cn0KdGV4dCAlPiUKICByZWNpcGUofi4pICU+JSAKICBzdGVwX3Rva2VuaXplKHRleHQsIGVuZ2luZSA9ICJzcGFjeXIiKSAlPiUKICBzdGVwX3Bvc19maWx0ZXIodGV4dCwga2VlcF90YWdzID0gIk5PVU4iKSAlPiUKICBzdGVwX2xlbW1hKHRleHQpICU+JQogIHN0ZXBfdGYodGV4dCkgJT4lCiAgcHJlcCgpICU+JQogIGp1aWNlKCkKYGBgCgoqIEEgbGFzdCByZW1pbmRlciBvbiB0aGUgcG93ZXJmdWwgYHBhaXJ3aXNlX3h4KClgIGZ1bmN0aW9ucyBmcm9tIHRoZSBgd2lkeXJgIHBhY2thZ2UKKiBGb3IgaW5zdGFuY2UsIHBhaXJ3aXNlIHNpbWlsYXJpdGllcy9kaXN0YW5jZXMKCmBgYHtyfQpsaWJyYXJ5KHdpZHlyKQpgYGAKCmBgYHtyfQp0ZXh0X3RpZHkgJT4lIHBhaXJ3aXNlX2Rpc3QoaWQsIHdvcmQsIHRmX2lkZiwgbWV0aG9kID0gIm1hbmhhdHRhbiIpICU+JQogIG11dGF0ZShzaW1pbGFyaXR5ID0gMSAtIChkaXN0YW5jZSAvIG1heChkaXN0YW5jZSkpICkgJT4lCiAgc2VsZWN0KC1kaXN0YW5jZSkgJT4lCiAgYXJyYW5nZShkZXNjKHNpbWlsYXJpdHkpKQpgYGAKCgojIERpbWVuc2lvbmFsaXR5IHJlZHVjdGlvbiB0ZWNobmlxdWVzCgpgYGB7cn0Kcm0obGlzdD1scygpKQpgYGAKCiogT2ssIGxldHMgZ2V0IGZpcnN0IHNvbWUgbW9yZSBpbnRlcmVzdGluZyBkYXRhLiBXZSB3aWxsIHdvcmsgd2l0aCB0aGUgQ09SRElTIHByb2plY3QgZGVzY3JpcHRpb25zIG9mIEVVIEhvcml6b24gMjAyMCBwcm9qZWN0cyBhZ2Fpbi4KCmBgYHtyfQp0ZXh0IDwtIHJlYWRfY3N2KCdodHRwczovL2dpdGh1Yi5jb20vU0RTLUFBVS9TRFMtbWFzdGVyL3Jhdy9tYXN0ZXIvTTIvZGF0YS9jb3JkaXMtaDIwMjByZXBvcnRzLmd6JykKYGBgCgpgYGB7cn0KY29sbmFtZXModGV4dCkgPC0gY29sbmFtZXModGV4dCkgJT4lIHN0cl90b19sb3dlcigpCnRleHQgJTw+JQogIHNlbGVjdCgteDEpICU+JQogIHJlbmFtZShpZCA9IHByb2plY3RpZCkgJT4lCiAgcmVsb2NhdGUoaWQpICU+JQogIGZpbHRlcihsYW5ndWFnZSA9PSAnZW4nKSAlPiUKICBkcm9wX25hKGlkKQpgYGAKCiogTGV0cyBjcmVhdGUgYSB0aWR5IHRva2VubGlzdAoKYGBge3J9CnRleHRfdGlkeSA8LSB0ZXh0ICU+JQogIHJlbmFtZSh0ZXh0ID0gc3VtbWFyeSkgJT4lCiAgc2VsZWN0KGlkLCB0ZXh0KSAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQsIHRva2VuID0gIndvcmRzIikKYGBgCgoqIHNvbWUgcHJlcHJvY2Vzc2luZwoKYGBge3J9CiMgcHJlcHJvY2Vzc2luZwp0ZXh0X3RpZHkgJTw+JQogIGZpbHRlcihzdHJfbGVuZ3RoKHdvcmQpID4gMiApICU+JSAjIFJlbW92ZSB3b3JkcyB3aXRoIGxlc3MgdGhhbiAgMyBjaGFyYWN0ZXJzCiAgZmlsdGVyKCEod29yZCAlaW4lIGMoJ3Byb2plY3QnLCAncmVzZWFyY2gnKSkpICU+JQogIGFudGlfam9pbihzdG9wX3dvcmRzLCBieSA9ICd3b3JkJykgCmBgYAoKKiBXZSBjYW4gYWxzbyBhZCBiaWdyYW1zCgpgYGB7cn0KdGV4dF90aWR5ICU8PiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHdvcmQsIHRva2VuID0gJ25ncmFtcycsIG4gPSAyLCBuX21pbiA9IDEpICU+JQogIGdyb3VwX2J5KHdvcmQpICU+JSBmaWx0ZXIobigpID4gMjUpICU+JSB1bmdyb3VwKCkgCmBgYAoKYGBge3J9CnRleHRfdGlkeSAlPiUKICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSkKYGBgCgoqIExldHMgZmluaXNoIHRoaXMgdXAgYW5kIGFsc28gYWRkIFRGLUlERiB3ZWlnaHRzCgpgYGB7cn0KdGV4dF90aWR5ICU8PiUKICBjb3VudChpZCwgd29yZCkgJT4lCiAgYmluZF90Zl9pZGYodGVybSA9IHdvcmQsCiAgICAgICAgICAgICAgZG9jdW1lbnQgPSBpZCwKICAgICAgICAgICAgICBuID0gbikgJT4lCiAgc2VsZWN0KC10ZiwgLWlkZikKYGBgCgoqIElzIHRoZXJlIGEgYmlnIGRpZmZlcmVuY2U/CgpgYGB7cn0KdGV4dF90aWR5ICU+JQogIGNvdW50KHdvcmQsIHd0ID0gdGZfaWRmLCBzb3J0ID0gVFJVRSkKYGBgCgoqIEFuZCBmaW5hbGx5LCBsZXRzIGdldCBhIERUTSBkYXRhZnJhbWUgCgpgYGB7cn0KdGV4dF9kdG0gPC0gdGV4dF90aWR5ICU+JQogIHNlbGVjdChpZCwgd29yZCwgbikgJT4lCiAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHdvcmQsIHZhbHVlc19mcm9tID0gbiwgdmFsdWVzX2ZpbGwgPSAwKQpgYGAKCiogQW5kLCBqdXN0IGluIGNhc2UsIGEgVEZJREYgd2VpZ2h0ZWQgdmVyc2lvbgoKYGBge3IsIGluY2x1ZGU9RkFMU0V9CnRleHRfZHRtX3RmX2lkZiA8LSB0ZXh0X3RpZHkgJT4lCiAgc2VsZWN0KGlkLCB3b3JkLCB0Zl9pZGYpICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSB3b3JkLCB2YWx1ZXNfZnJvbSA9IHRmX2lkZiwgdmFsdWVzX2ZpbGwgPSAwKQpgYGAKCiogV2UgY291bGQgYWxzbyBwcmVwYXJlIGEgcmVjaXBlIHdoaWNoIGRvZSBwcmV0dHkgbXVjaCB0aGUgc2FtZS4uLgoKYGBge3J9CnJlY2lwZV9iYXNlIDwtIHRleHQgJT4lCiAgcmVuYW1lKHRleHQgPSBzdW1tYXJ5KSAlPiUKICBzZWxlY3QoaWQsIHRleHQpICU+JQogICMgQkFzZSByZWNpcGUgc3RhcnRzCiAgcmVjaXBlKH4uKSAlPiUgCiAgdXBkYXRlX3JvbGUoaWQsIG5ld19yb2xlID0gImlkIikgJT4lICMgVXBkYXRlIHJvbGUgb2YgSUQKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lICMgdG9rZW5pemUKICBzdGVwX3N0b3B3b3Jkcyh0ZXh0LCBrZWVwID0gRkFMU0UpICU+JSAjIHJlbW92ZSBzdG9wd29yZHMKICBzdGVwX3VudG9rZW5pemUodGV4dCkgJT4lICMgSGVyZSB3ZSBub3cgaGF2ZSB0byBmaXJzdCB1bnRva2VuaXplCiAgc3RlcF90b2tlbml6ZSh0ZXh0LCB0b2tlbiA9ICJuZ3JhbXMiLCBvcHRpb25zID0gbGlzdChuID0gMSwgbl9taW4gPSAxKSkgJT4lICMgYW5kIHRva2VuaXplIGFnYWluCiAgc3RlcF90b2tlbmZpbHRlcih0ZXh0LCBtaW5fdGltZXMgPSAyNSkgCmBgYAoKKiBTaWRlbm90ZQoKKiBIZXJlLCB3ZSBjYW4gZnVydGhlciBwcmVwcm9jZXNzIHRvIGRvIHdoYXRldmVyIHdlIHdvdWxkIGxpa2UsIHN1Y2ggYXMgb2J0YWluaW5nIGEgZHRtCgpgYGB7cn0KcmVjaXBlX2Jhc2UgJT4lIAogIHN0ZXBfdGYodGV4dCkgJT4lIAogIHByZXAoKSAlPiUgCiAganVpY2UoKSAlPiUgCiAgaGVhZCgxMDApCmBgYAoKYGBge3J9CnRleHRfcGNhIDwtIHRleHRfZHRtICU+JSAKICBjb2x1bW5fdG9fcm93bmFtZXMoJ2lkJykgJT4lIAogIHByY29tcChjZW50ZXIgPSBUUlVFLCBzY2FsZS4gPSBUUlVFLCByYW5rLiA9IDEwKQpgYGAKCmBgYHtyfQp0ZXh0X3BjYSAlPiUgZ2xpbXBzZSgpCmBgYAoKYGBge3J9CnRleHRfcGNhW1sneCddXSAlPiUKICBoZWFkKCkKYGBgCgoKYGBge3J9CnRleHRfcGNhICU+JSB0aWR5KCkKYGBgCgoKKiBBZ2FpbiwgYWx0ZXJuYXRpdmVseSB3aXRoIGEgcmVjaXBlLi4uCgpgYGB7cn0KcmVjaXBlX3BjYSA8LSByZWNpcGVfYmFzZSAlPiUgIyB0b2tlbml6ZQogIHN0ZXBfdGZpZGYodGV4dCwgcHJlZml4ID0gJycpICU+JSAjIFRGSURGIHdlaWdodGluZwogIHN0ZXBfcGNhKGFsbF9wcmVkaWN0b3JzKCksIG51bV9jb21wID0gMTApICU+JSAjIFBDQQogIHByZXAoKSAKYGBgCgpgYGB7cn0KcmVjaXBlX3BjYSAlPiUganVpY2UoKQpgYGAKKiBTb21lIHBsb3R0aW5nCgpgYGB7cn0KcmVjaXBlX3BjYSAlPiUganVpY2UoKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBQQzAxLCB5ID0gUEMwMikpICsKICBnZW9tX3BvaW50KCkgCmBgYAoqIHdlIGNhbiBhbHNvIHVzZSB0aGUgdGlkeSByZXN1bHRzIG9mIHRoZSByZWNpcGUgdG8gZG8gc29tZSBtb3JlIGFuYWx5dGljcwoKYGBge3J9CnJlY2lwZV9wY2EgJT4lCiAgdGlkeSg3KSAlPiUKICBmaWx0ZXIoY29tcG9uZW50ICVpbiUgcGFzdGUwKCJQQyIsIDE6NCkpICU+JQogIGdyb3VwX2J5KGNvbXBvbmVudCkgJT4lCiAgICBhcnJhbmdlKGRlc2ModmFsdWUpKSAlPiUKICAgIHNsaWNlKGMoMToyLCAobigpLTIpOm4oKSkpICU+JQogIHVuZ3JvdXAoKSAlPiUKICBtdXRhdGUoY29tcG9uZW50ID0gZmN0X2lub3JkZXIoY29tcG9uZW50KSkgJT4lCiAgZ2dwbG90KGFlcyh2YWx1ZSwgdGVybXMsIGZpbGwgPSB0ZXJtcykpICsKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgZmFjZXRfd3JhcCh+Y29tcG9uZW50LCBucm93ID0gMSkgKwogIGxhYnMoeSA9IE5VTEwpCmBgYAoKKiAqKk5vdGUqKjogQWxzbyBjaGVjayBmdXJ0aGVyIGZvciBmdXJ0aGVyIGRpbWVuc2lvbmxpdHkgcmVkdWN0aW9uIHN0ZXBzOgogICAqIHRlcF9rcGNhKCk6CiAgICogc3RlcF9pY2EoKQogICAqIHN0ZXBfaXNvbWFwKCkKICAgKiBzdGVwX25ubWYoKQogICAKCiMgVG9waWMgTW9kZWxzOiBMYXRlbnQtRGlyaWNobGV0LUFsbG9jYXRpb24gKExEQSkKCiogV2hpbGUgd2UgYWxyZWFkeSBkaWQgaXQgc29tZXdoYXQgJ29uLXRoZS1mbHknLCBoZXJlIGEgbW9yZSBmb3JtYWwgaW50cm9kdWN0aW9uIHRvIExEQQoqIEluIGNvbnRyYXN0IHRvIGRpbW5lc2lvbmFsaXR5IHJlZHVjdGlvbiB0ZWNoaXF1ZXMgbW9zdGx5IGFpbWluZyBhdCBwcmVwcm9jZXNzaW5nIGRhdGEgb3IgZWFzaW5nIHZpc3VhbGl6YXRpb24sIExEQSBtb3JlIGFpbXMgYXQgRURBIGFuZCBpbnRlcnByZXRhdGlvbgoqIEl0IGlzIGEgZ2VuZXJhdGl2ZSBhcHByb2FjaCB0byBpZGVudGlmeSB0b3BpY3MgKGNsdXN0ZXJzKSB3aXRoaW4gdGhlIHdvcmQtdXNhZ2UgaW4gZG9jdW1lbnRzLgogICAqIFRvcGljcyBhcmUgcmVwcmVzZW50ZWQgYXMgYSBwcm9iYWJpbGl0eSBkaXN0cmlidXRpb24gb3ZlciB0aGUgd29yZHMgaW4gdGhlIHZvY2FidWxhcnkuIEhoaWdoIHByb2JhYmlsaXR5IHdvcmRzIGNhbiBiZSB1c2VkIHRvIGNoYXJhY3RyaXplIHRoZSB0b3BpYy4KICAgKiBEb2N1bWVudHMgYXJlIHJlcHJlc2VudGVkIGFzIGEgbWl4dHVyZSBvZiB0b3BpY3MuCgohW2FsdCB0ZXh0XShodHRwczovL21pcm8ubWVkaXVtLmNvbS9tYXgvMTYwMC8xKnBab19JY3hXMUdWdUgydlFLZG9JTVEuanBlZykKCmBgYHtyfQpsaWJyYXJ5KHRvcGljbW9kZWxzKQpgYGAKCgpgYGB7cn0KdGV4dF9kdG0gPC0gdGV4dF90aWR5ICU+JQogIGNhc3RfZHRtKGRvY3VtZW50ID0gaWQsIHRlcm0gPSB3b3JkLCB2YWx1ZSA9IG4pCmBgYAoKYGBge3J9CnRleHRfbGRhIDwtIHRleHRfZHRtICU+JSAKICBMREEoayA9IDYsIG1ldGhvZCA9ICJHaWJicyIsCiAgICAgIGNvbnRyb2wgPSBsaXN0KHNlZWQgPSAxMzM3KSkKYGBgCgoKKiAkXGJldGEkIGlzIGFuIG91dHB1dCBvZiB0aGUgTERBIG1vZGVsLCBpbmRpY2F0aW5nIHRoZSBwcm9wYWJpbGl0eSB0aGF0IGEgd29yZCBvY2N1cnMgaW4gYSBjZXJ0YWluIHRvcGljLgoqIFRoZXJlZm9yZSwgbG9raW5nIGF0IHRoZSB0b3AgcHJvYmFiaWxpdHkgd29yZHMgb2YgYSB0b3BpYyBvZnRlbiBnaXZlcyB1cyBhIGdvb2QgaW50dWl0aW9uIHJlZ2FyZGluZyBpdHMgcHJvcGVydGllcy4KCmBgYHtyfQojIExEQSBvdXRwdXQgaXMgZGVmaW5lZCBmb3IgdGlkeSgpLCBzbyB3ZSBjYW4gZWFzaWx5IGV4dHJhY3QgaXQKbGRhX2JldGEgPC0gdGV4dF9sZGEgJT4lIAogIHRpZHkobWF0cml4ID0gImJldGEiKSAKYGBgCgpgYGB7cn0KbGRhX2JldGEgJT4lCiAgIyBzbGljZQogIGdyb3VwX2J5KHRvcGljKSAlPiUKICBhcnJhbmdlKHRvcGljLCBkZXNjKGJldGEpKSAlPiUKICBzbGljZSgxOjEwKSAlPiUKICB1bmdyb3VwKCkgJT4lCiAgIyB2aXN1YWxpemUKICBtdXRhdGUodGVybSA9IHJlb3JkZXJfd2l0aGluKHRlcm0sIGJldGEsIHRvcGljKSkgJT4lCiAgZ3JvdXBfYnkodG9waWMsIHRlcm0pICU+JSAgICAKICBhcnJhbmdlKGRlc2MoYmV0YSkpICU+JSAgCiAgdW5ncm91cCgpICU+JQogIGdncGxvdChhZXModGVybSwgYmV0YSwgZmlsbCA9IGFzLmZhY3Rvcih0b3BpYykpKSArCiAgZ2VvbV9jb2woc2hvdy5sZWdlbmQgPSBGQUxTRSkgKwogIGNvb3JkX2ZsaXAoKSArCiAgc2NhbGVfeF9yZW9yZGVyZWQoKSArCiAgbGFicyh0aXRsZSA9ICJUb3AgMTAgdGVybXMgaW4gZWFjaCBMREEgdG9waWMiLAogICAgICAgeCA9IE5VTEwsIHkgPSBleHByZXNzaW9uKGJldGEpKSArCiAgZmFjZXRfd3JhcCh+IHRvcGljLCBuY29sID0gMywgc2NhbGVzID0gImZyZWUiKQpgYGAKCiogRG9jdW1lbnRzIGFyZSByZXByZXNlbnRlZCBhcyBhIG1peCBvZiB0b3BpY3MuIFRoaXMgYXNzb2NpYXRpb24gb2YgYSBkb2N1bWVudCB0byBhIHRvcGljIGlzIGNhcHR1cmVkIGJ5ICRcZ2FtbWEkCgpgYGB7cn0KbGRhX2dhbW1hIDwtIHRleHRfbGRhICU+JSAKICB0aWR5KG1hdHJpeCA9ICJnYW1tYSIpCmBgYAoKCmBgYHtyfQpsZGFfZ2FtbWEgJT4lCiAgZ3JvdXBfYnkodG9waWMpICU+JQogICAgYXJyYW5nZShkZXNjKGdhbW1hKSkgJT4lIAogICAgc2xpY2UoMToxMCkgJT4lCiAgdW5ncm91cCgpICU+JQogIGxlZnRfam9pbih0ZXh0ICU+JSBzZWxlY3QoaWQsIHByb2plY3RhY3JvbnltKSAlPiUgbXV0YXRlKGlkID0gaWQgJT4lIGFzLmNoYXJhY3RlcigpKSwgYnkgPSBjKCdkb2N1bWVudCcgPSAnaWQnKSkKYGBgCgoqIE5vdGUgdGhhdCBhbiBMREEgY2FuIGFsc28gYmUgcGVyZm9ybWVkIHZpYSBhIHJlY2lwZToKCmBgYHtyfQpyZWNpcGVfbGRhIDwtIHJlY2lwZV9iYXNlICU+JSAjIHRva2VuaXplCiAgc3RlcF9sZGEodGV4dCwgbnVtX3RvcGljcyA9IDYpICU+JSAjIExEQQogIHByZXAoKSAKYGBgCgpgYGB7cn0KcmVjaXBlX2xkYSAlPiUganVpY2UoKSAlPiUgCiAgaGVhZCgxMDApCmBgYAoKKiBBcyBhIGJvbnVzLCBhIGdyZWF0IHdheSB0byBpbnRlcmFjdGl2ZWx5IHZpc3VhbGl6ZSBMREEncy4KKiBJdCdzIGEgYml0IGN1bWJlcnNvbWUgaW4gUiwgdGhvdWdoLi4uCgpgYGB7cn0KbGlicmFyeShMREF2aXMpCmBgYAoKCmBgYHtyfQojIEEgYml0IG9mIGEgbGVuZ2h0eSBmdW5jdGlvbi4uLi4KdG9waWNtb2RlbHNfanNvbl9sZGF2aXMgPC0gZnVuY3Rpb24oZml0dGVkLCBkb2NfZHRtLCBtZXRob2QgPSAiUENBIil7CiAgcmVxdWlyZSh0b3BpY21vZGVscyk7IHJlcXVpcmUoZHBseXIpOyByZXF1aXJlKExEQXZpcykKICAKICAjIEZpbmQgcmVxdWlyZWQgcXVhbnRpdGllcwogIHBoaSA8LSBwb3N0ZXJpb3IodGV4dF9sZGEpJHRlcm1zICU+JSBhcy5tYXRyaXgoKSAjIFRvcGljLXRlcm0gZGlzdHJpYnV0aW9uCiAgdGhldGEgPC0gcG9zdGVyaW9yKGZpdHRlZCkkdG9waWNzICU+JSBhcy5tYXRyaXgoKSAjIERvY3VtZW50LXRvcGljIG1hdHJpeAogIAogIHRleHRfdGlkeSA8LSBkb2NfZHRtICU+JSB0aWR5KCkKICB2b2NhYiA8LSBjb2xuYW1lcyhwaGkpCiAgZG9jX2xlbmd0aCA8LSB0aWJibGUoZG9jdW1lbnQgPSByb3duYW1lcyh0aGV0YSkpICU+JSBsZWZ0X2pvaW4odGV4dF90aWR5ICU+JSBjb3VudChkb2N1bWVudCwgd3QgPSBjb3VudCksIGJ5ID0gJ2RvY3VtZW50JykKICB0ZiA8LSB0aWJibGUodGVybSA9IHZvY2FiKSAlPiUgbGVmdF9qb2luKHRleHRfdGlkeSAlPiUgY291bnQodGVybSwgd3QgPSBjb3VudCksIGJ5ID0gInRlcm0iKSAKICAKICBpZihtZXRob2QgPT0gIlBDQSIpe21kcyA8LSBqc1BDQX0KICBpZihtZXRob2QgPT0gIlRTTkUiKXtsaWJyYXJ5KHRzbmUpOyBtZHMgPC0gZnVuY3Rpb24oeCl7dHNuZShzdmQoeCkkdSl9IH0KICAKICAjIENvbnZlcnQgdG8ganNvbgogIGpzb25fbGRhIDwtIExEQXZpczo6Y3JlYXRlSlNPTihwaGkgPSBwaGksIHRoZXRhID0gdGhldGEsIHZvY2FiID0gdm9jYWIsIGRvYy5sZW5ndGggPSBkb2NfbGVuZ3RoICU+JSBwdWxsKG4pLCB0ZXJtLmZyZXF1ZW5jeSA9IHRmICU+JSBwdWxsKG4pLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZW9yZGVyLnRvcGljcyA9IEZBTFNFLCBtZHMubWV0aG9kID0gbWRzLHBsb3Qub3B0cyA9IGxpc3QoeGxhYiA9ICJEaW0uMSIsIHlsYWIgPSAiRGltLjIiKSkgCiAgcmV0dXJuKGpzb25fbGRhKQp9CmBgYAoKCmBgYHtyfQpsaWJyYXJ5KExEQXZpcykKanNvbl9sZGEgPC0gdG9waWNtb2RlbHNfanNvbl9sZGF2aXMoZml0dGVkID0gdGV4dF9sZGEsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkb2NfZHRtID0gdGV4dF9kdG0sIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiVFNORSIpCgpqc29uX2xkYSAlPiUgc2VyVmlzKCkgIyBGb3IgZGlyZWN0IG91dHB1dAojIGpzb25fbGRhICU+JSBzZXJWaXMob3V0LmRpciA9ICdMREF2aXonKSAjIEZvciBzYXZpbmcgdGhlIGh0bWwKYGBgCgoKPGlmcmFtZSB3aWR0aD0iMTIwMCIgaGVpZ2h0PSIxNTAwIiBzcmM9Imh0dHBzOi8vZGFuaWVsLWhhaW4uZ2l0aHViLmlvL01MX2NvdXJzZV9tYWFzdHJpY2h0L25vdGVib29rcy9MREF2aXovaW5kZXguaHRtbCIgZnJhbWVib3JkZXI9IjAiPjwvaWZyYW1lPiAKCkRpZG50IHJlYWxseSBmaWd1cmUgb3V0IGhvdyB0byBlbWJlZGQgdGhlIHJlc3VsdGluZyBwbG90LCBidXQgdGhlIG91dGNvbWUgY2FuIGJlIHNlZW4gW2hlcmVdKGh0dHBzOi8vZGFuaWVsLWhhaW4uZ2l0aHViLmlvL01MX2NvdXJzZV9tYWFzdHJpY2h0L25vdGVib29rcy9MREF2aXovaW5kZXguaHRtbCkKCgojIEVtYmVkZGluZ3MgKEJvbnVzKQoKKiBPbmUgbGFzdCB0aGluZyB3ZSBkaWQgbm90IHZlbnR1cmUgaW4geWV0LCBhcmUgZW1iZWRkaW5ncwoqIEkgd2lsbCBub3QgZ28gaW50byBkZXRhaWxzIGhlcmUsIGp1c3Qgc2VlIGl0IGFzIGEgcGVhayBvZiB3aGF0J3MgdG8gY29tZSBpbiBmdXJ0aGVyIHNlc3Npb25zLgoqIFRoZSBpZGVlIG9mIHdvcmQgZW1iZWRkaW5nIGlzIChpbiBhIG51dHNoZWxsKSB0aGF0CgoKKiBUaGVyZSBhcmUgcGFja2FnZXMgb24gaG93IHRvIHRyYWluIG93biBlbWJlZGRpbmdzIHN1Y2ggYXMgW2B0ZXh0MnZlY2BdKGh0dHA6Ly90ZXh0MnZlYy5vcmcvKSwgYnV0IHdlIHdpbGwgZm9yIG5vdyBub3QgYm90aGVyIHdpdGggdGhhdC4KKiBUaGUgb25seSB0aGluZyB3ZSB3aWxsIGRvIGZvciBub3cgaXMgdG8gbG9hZCBwcmV0cmFpbmVkIGVtYmVkZGluZ3MgKEdsb1ZlLCBjZi4gUGVubmluZ3RvbiBldCBhbCwgMjAxNCkKCgpgYGB7cn0KbGlicmFyeSh0ZXh0ZGF0YSkKCmdsb3ZlNmIgPC0gZW1iZWRkaW5nX2dsb3ZlNmIoZGltZW5zaW9ucyA9IDEwMCkKZ2xvdmU2YgpgYGAKCgoqIExhIHZvaWxhLCBhIGxhcmdlIHByZXRyYWluZWQgZW1iZWRkaW5nIG1vZGVsIGZvciBhcm91bmQgNDAwayBvZiB0aGUgbW9zdCBjb21tb24gd29yZHMuIAoqIFdlIGZvciBub3cgbG9hZGVkIHRoZSBzbWFsbGVzdCBvZiB0aGVzZSBlbWJlZGRpbmcgbW9kZWxzLCB0aGVyZSBleGlzdCB3YXkgYmlnZ2VyIG9uZXMuCiogTGV0cyBqb2luIGl0IHdpdGggb3VyIHRpZHkgdG9rZW5saXN0CgpgYGB7cn0Kd29yZF9lbWJlZGRpbmdzIDwtIHRleHRfdGlkeSAlPiUKICBpbm5lcl9qb2luKGdsb3ZlNmIsIGJ5ID0gYygnd29yZCcgPSAndG9rZW4nKSkKYGBgCgpgYGB7cn0Kd29yZF9lbWJlZGRpbmdzICU+JSBoZWFkKCkKYGBgCgoqIFdlIGNvdWxkIG5vdyBjcmVhdGUgYXZlcmFnZSBkb2N1bWVudCBlbWJlZGRpbmdzIGJ5IHRha2luZyB0aGUgbWVhbiBvdmVyIGFsbCBkaW1lbnNpb25zCiogV2UgY291bGQgYWxzbyAoZXZlbiBiZXR0ZXIpIHdlaWdodCB0aGF0IGJ5IHRoZW4gd29yZCdzIHRmaWRmIHNjb3JlLgoKYGBge3J9CmRvY19lbWJlZGRpbmdzIDwtIHdvcmRfZW1iZWRkaW5ncyAlPiUKICBncm91cF9ieShpZCkgJT4lCiAgc3VtbWFyaXNlKGFjcm9zcyhzdGFydHNfd2l0aCgiZCIpLCB+bWVhbigueCAvIHRmX2lkZiwgbmEucm0gPSBUUlVFKSkpCmBgYAoKKiBUaGVzZSBlbWJkZGluZ3MgY291bGQgbm93IGJlIHVzZWQgZm9yIGluc3RhbmNlIGZvciBzb21lIGNsdXN0ZXJpbmcgb3IgU01MIGV4ZXJjaXNlCiogSSBndWVzcyB5b3UgY2FuIGFscmVhZHkgc2VlIGhvdyB0byB1c2UgdGhlc2UgZW1iZWRkaW5ncyBpbiBhbiBTTUwgbW9kZWwuCgpgYGB7cn0KbGlicmFyeSh1d290KSAjIGZvciBVTUFQCmBgYAoKCmBgYHtyfQplbWJlZGRpbmdzX3VtYXAgPC0gZG9jX2VtYmVkZGluZ3MgICU+JSAKICBjb2x1bW5fdG9fcm93bmFtZXMoImlkIikgJT4lCiAgdW1hcChuX25laWdoYm9ycyA9IDE1LCAKICAgICAgIG1ldHJpYyA9ICJjb3NpbmUiLCAKICAgICAgIG1pbl9kaXN0ID0gMC4wMSwgCiAgICAgICBzY2FsZSA9IFRSVUUsCiAgICAgICB2ZXJib3NlID0gVFJVRSwgCiAgICAgICBuX3RocmVhZHMgPSA4KSAKYGBgCgpgYGB7cn0KZW1iZWRkaW5nc191bWFwICU8PiUgYXMuZGF0YS5mcmFtZSgpCmBgYAoKCmBgYHtyfQplbWJlZGRpbmdzX3VtYXAgICU+JSAKICBnZ3Bsb3QoYWVzKHggPSBWMSwgeSA9IFYyKSkgKyAKICBnZW9tX3BvaW50KHNoYXBlID0gMjEsIGFscGhhID0gMC41KSAKYGBgCgoqIE9rLCB3ZSBzZWUgYSByYXRoZXIgY2xlYXIgc2VwZXJhdGlvbiBvZiBkb2N1bWVudHMuCiogSnVzdCBmb3IgZnVuLCBsZXRzIGFkZCBhIGRlbnNpdHkgYmFzZWQgY2x1c3RlcmluZyAodmVyeSBnb29kIGZvciBzcGF0aWFsIGNsdXN0ZXJpbmcpIG9uIHRvcCAoZXZlbiB0aG91Z2ggd2UgYWxyZWFkeSBzZWUgdGhlIHJlc3VsdHMpCgpgYGB7cn0KbGlicmFyeShkYnNjYW4pCmBgYAoKKiBEbyB0aGUgaGlyYXJjaGljYWwgZGVuc2l0eSBiYXNlZCBjbHVzdGVyaW5nCiAgICAgICAKYGBge3J9CmVtYmVkZGluZ3NfaGRic2NhbiA8LSBlbWJlZGRpbmdzX3VtYXAgJT4lIGFzLm1hdHJpeCgpICU+JSBoZGJzY2FuKG1pblB0cyA9IDE1KQpgYGAKCiogUGxvdCBpdAoKYGBge3J9CmVtYmVkZGluZ3NfdW1hcCAlPiUgCiAgYmluZF9jb2xzKGNsdXN0ZXIgPSBlbWJlZGRpbmdzX2hkYnNjYW4kY2x1c3RlciAlPiUgYXMuZmFjdG9yKCksIAogICAgICAgICAgICBwcm9iID0gZW1iZWRkaW5nc19oZGJzY2FuJG1lbWJlcnNoaXBfcHJvYikgJT4lCiAgZ2dwbG90KGFlcyh4ID0gVjEsIHkgPSBWMiwgY29sID0gY2x1c3RlcikpICsgCiAgZ2VvbV9wb2ludChhZXMoYWxwaGEgPSBwcm9iKSwgc2hhcGUgPSAyMSkgCmBgYAoKKiBOb3RlOiBXZSBjYW4gYWxzbyBhc3NpZ25lIHRoZSBlbWJlZGRpbmdzIHZpYSBhIHJlY2lwZQoqIFVuZm9ydHVuYXRlbHksIHdlIGNhbiBub3QgZG8gYSBURklERiB3ZWlnaHRpbmcgaGVyZSAnb3V0LW9mLXRoZS1ib3gnLCBidXQgaGF2ZSB0byB3b3JrIHdpdGggYXZlcmFnZSBlbWJlZGRpbmdzIGluc3RlYWQuCgoKYGBge3J9CnJlY2lwZV9lbWJlZGRpbmcgPC0gcmVjaXBlX2Jhc2UgJT4lICMgdG9rZW5pemUKICBzdGVwX3dvcmRfZW1iZWRkaW5ncyh0ZXh0LCBlbWJlZGRpbmdzID0gZ2xvdmU2YiwgYWdncmVnYXRpb24gPSAnbWVhbicpCmBgYAoKYGBge3J9CnJlY2lwZV9lbWJlZGRpbmcgJT4lIHByZXAoKSAlPiUganVpY2UoKSAlPiUgCiAgaGVhZCgxMDApCmBgYAoKCjwhLS0tCiogU2FtZSBnb2VzIGZvciBVTUFQLCB3aGljaCBjYW4gYmUgYWNjZXNzZCBpbiByZWNpcGVzIHZpYSB0aGUgdGhlIHBhY2thZ2UgYGVtYmVkYCBwY2thZ2UuCiogSG93ZXZlcixgZW1iZWRgIGlzIGEgYml0IGhlYXZ5IGluIHRlcm1zIG9mIGRlcGVuZGVuY2llcywgc2luY2UgaXQgdXNlcyBga2VyYXNgIGFuZCBgdGVuc29yZmxvd2AsIGEgZGVlcCBsZWFybmluZyBmcmFtZXdvaywgaW4gdGhlIGJhY2tncm91Ym5kLCBhbmQgaXMgaW4gbmVlZCB0byBpbnN0YWxsIGFub3RoZXIgbWluaS1jb25kYSBlbnZpcm9tZW50LiAKKiBJZiB5b3UgaGF2ZSBubyBleHBlcmllbmNlIHdpdGggYGtlcmFzYCBhbmQgYHRlbnNvcmZsb3dgIHNvIGZhciwgSSBzdWdnZXN0IHlvdSB3YWl0IHdpdGggdGhpcyBvbmUgdW50aWwgbGF0ZXIgc2Vzc2lvbnMgd2hlbiB3ZSBwcm9wZXJseSBpbnRyb2R1Y2UgaXQuCgpgYGB7cn0KbGlicmFyeShlbWJlZCkKYGBgCgpgYGB7cn0KcmVjaXBlX3VtYXAgPC0gcmVjaXBlX2VtYmVkZGluZyAlPiUKICBzdGVwX3VtYXAoc3RhcnRzX3dpdGgoJ3dfZW1iZWQnKSwgbl9uZWlnaGJvcnMgPSAxNSkgCmBgYAoKYGBge3J9CnJlY2lwZV91bWFwICU+JSBwcmVwKCkgJT4lIGp1aWNlKCkgJT4lIAogIGhlYWQoMTAwKQpgYGAKCi0tLT4KCiogU28sIHRoYXQncyBhbGwgSSBoYXZlIGZvciBub3cKCiMgU3VtbWFyeQoKKiBUaGVyZSBhcmUgbWFueSB3YXlzIHRvIGNvbnZlcnQgdGV4dCBkYXRhIGludG8gYSB2ZWN0b3IgcmVwcmVzZW50YXRpb24uCiogVGhlc2UgcmFuZ2UgZnJvbSBzaW1wbGUgYW5kIHdlaWdodGVkIGJhZ3Mtb2Ytd29yZHMsIHRvIHRvcGljIG1vZGVscywgb3ZlciBkaWZmZXJlbnQgdHlwZXMgb2YgZGltZW5zaW9uYWxpdHkgcmVkdWN0aW9uIHRvIGZpbmFsbHkgd29yZCBhbmQgZG9jdW1lbnQgZW1iZWRkaW5ncy4KKiBBbGwgb2YgdGhlbSBhcmUgdXNlZnVsLCBkZXBlbmRpbmcgb24gdGhlIHB1cnBvc2UuCgojIEVuZG5vdGVzCgojIyMgUGFja2FnZXMgJiBFY29zeXN0ZW0KCiogW2B0ZXh0cmVjaXBlc2BdKGh0dHBzOi8vdGV4dHJlY2lwZXMudGlkeW1vZGVscy5vcmcvKTogVGV4dCBwcmVwcm9jZXNzaW5nIHJlY2lwZXMKKiBbYGVtYmVkYF0oaHR0cHM6Ly9lbWJlZC50aWR5bW9kZWxzLm9yZy8pOiBFeHRyYSBlbWJlZGRpbmcgcmVjaXBlcwoqIFtgdG9waWNtb2RlbHNgXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvdG9waWNtb2RlbHMvdmlnbmV0dGVzL3RvcGljbW9kZWxzLnBkZik6IExEQSB0b3BpY21vZGVsbGluZyBpbiBSCiogW2BMREF2aXNgXShodHRwczovL2dpdGh1Yi5jb20vY3BzaWV2ZXJ0L0xEQXZpeik6IEEgYml0IGNsdW5reSBidXQgYXdlc29tZSBpbnRlcmFjdGl2ZSBMREEgdmlzdWFsaXphdGlvbnMKKiBbYHRleHQydmVjYF0oaHR0cDovL3RleHQydmVjLm9yZy8pOiBQYWNrYWdlIHZvciB2ZWN0b3Igc3BhY2UgbW9kZWxsaW5nIChha2EgZW1iZWRkaW5ncyAmIG90aGVyIHZlY3Rvcml6YXRpb25zKSBvZiB0ZXh0ZGF0YQoqIFtgdGV4dGRhdGFgXShodHRwczovL2dpdGh1Yi5jb20vRW1pbEh2aXRmZWxkdC90ZXh0ZGF0YSk6IFVzZWZ1bCBkYXRhc2V0cyBmb3IgdGV4dCwgc3VjaCBhcyBHbG9WZSBlbWJlZGRpbmdzLCBzZW50aW1lbnQgbGV4aWNhIGV0Yy4KKiBbYHV3b3RgXShodHRwczovL2dpdGh1Yi5jb20vamxtZWx2aWxsZS91d290KTogVU1BUCBmb3IgUgoKIyMjIFJlZmVyZW5jZXMgCgpDSGFwdGVyczoKCiogSnVsaWEgU2lsZ2UgYW5kIERhdmlkIFJvYmluc29uICgyMDIwKS4gVGV4dCBNaW5pbmcgd2l0aCBSOiBBIFRpZHkgQXBwcm9hY2gsIE/igJlSZWlsbHkuIE9ubGluZSBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS8pCiAgICogW0NoYXB0ZXIgNl0oaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tL3RvcGljbW9kZWxpbmcuaHRtbCk6IHh4eAoqIEVtaWwgSHZpZGZlbGR0IGFuZCBKdWxpYSBTaWxnZSAoMjAyMCkuIFN1cGVydmlzZWQgTWFjaGluZSBMZWFybmluZyBmb3IgVGV4dCBBbmFseXNpcyBpbiBSLCBvbmxpbmUgYXZhaWxhYmxlIFtoZXJlXShodHRwczovL3NtbHRhci5jb20vKQogICAqIFtDaGFwdGVyIDVdKGh0dHBzOi8vc21sdGFyLmNvbS9lbWJlZGRpbmdzLmh0bWwpOiBXb3JkIEVtYmVkZGluZ3MKCgpBcnRpY2xlczoKKiBCbGVpLCBEYXZpZCBNLiwgQW5kcmV3IFkuIE5nLCBhbmQgTWljaGFlbCBJLiBKb3JkYW4uICJMYXRlbnQgZGlyaWNobGV0IGFsbG9jYXRpb24uIiBKb3VybmFsIG9mIG1hY2hpbmUgTGVhcm5pbmcgcmVzZWFyY2ggMywgbm8uIEphbiAoMjAwMyk6IDk5My0xMDIyLgoqIEplZmZyZXkgUGVubmluZ3RvbiwgUmljaGFyZCBTb2NoZXIsIGFuZCBDaHJpc3RvcGhlciBEIE1hbm5pbmcuIEdsb3ZlOiBHbG9iYWwgdmVjdG9ycyBmb3Igd29yZCByZXByZXNlbnRhdGlvbi4gSW4gQ29uZmVyZW5jZSBvbiBFbXBpcmljYWwgTWV0aG9kcyBvbiBOYXR1cmFsIExhbmd1YWdlIFByb2Nlc3NpbmcgKEVNTkxQKSwgcGFnZXMgMTUzMuKAkzE1NDMsIDIwMTQKCiMjIyBGdXJ0aGVyIHNvdXJjZXMKCiogW0p1bGlhIFNpbGdlJ3MgQmxvZ10oaHR0cHM6Ly9qdWxpYXNpbGdlLmNvbS8pOiBGdWxsIG9mIGdyZWF0IGV4YW1wbGVzIG9mIHByZWRpY3RpdmUgbW9kZWxpbmcsIE5MUCwgYW5kIHRoZSBjb21iaW5hdGlvbiBmbyBib3RoLCB1c2luZyB0aWR5IGVjb3N5c3RlbXMKKiBbRW1pbCBIdml0ZmVsZHQncyBCbG9nXShodHRwczovL3d3dy5odml0ZmVsZHQubWUvKTogTGlrZXdpc2UsIGZ1bGwgb2YgZ3JlYXQgZXhhbXBsZXMgb2YgYXBwbGllZCB0aWR5IE1MICYgTkxQIGluIAoKIyMjIFNlc3Npb24gSW5mbwoKYGBge3J9CnNlc3Npb25JbmZvKCkKYGBgCgoKCgoKCg==